⌨️

AutoHotkeyでNotepad++制御し、文字を括弧で囲むスクリプト作成

に公開

はじめに

この記事では、Notepad++を外部制御し、選択範囲の文字列を括弧で囲むAutoHotkey v2スクリプトを作成した話を紹介します。

Notepad++のエディタ部分は「Scintilla」というテキスト処理エンジンで構成されており、このスクリプトは、AutoHotkeyからScintillaコマンドを操作することで高度な制御を実現しています。

対象読者は、AutoHotkeyやWindows APIをある程度理解している中級者を想定しています。

必要な環境

  • AutoHotkey バージョン2
  • Notepad++

スクリプトの動作概要

Notepad++でテキストを選択した状態でショートカットキーを押すと、選択箇所が括弧付きテキストに置き換わります。

例として、 サンプル を選択してショートカットキーを押すと 「サンプル」 になり、もう一度押すと 『サンプル』 になり、さらに押すと括弧を外して最初に戻るような動作です。

内部的には括弧を付ける処理を行った後に、選択範囲を保持するようにしているため、繰り返し括弧の種類を切り替えられます。

技術的ポイント

  • AutoHotkeyからWindowsのSendMessage経由でScintillaコマンドを送信し、Notepad++のエディタ操作を実現。
  • テキストの送受信には、VirtualAllocExでNotepad++側プロセス空間にバッファを確保し、WriteProcessMemoryやReadProcessMemoryでデータを転送。
  • UTF-8テキストの処理では、必要なメモリ領域を事前に計算し、日本語を含む文字列でも安全に扱えるようにしている。
  • 選択範囲を再設定する仕組みにより、括弧切り替えを連続実行できる。

Scintillaコマンドの検証結果

検証の結果、次のように動作を確認しました。

  • SCI_GETCURRENTPOS、SCI_GETSELECTIONSTART、SCI_GETSELECTIONEND、SCI_SETSELはSendMessageだけで取得可能。
  • SCI_GETSELTEXTは、VirtualAllocExとReadProcessMemoryを併用する必要あり。
  • SCI_REPLACESELは、VirtualAllocExとWriteProcessMemoryを併用する必要あり。

これら6種類のコマンドの動作を実際に確認したことで、Scintilla経由で多くの操作をAutoHotkeyから扱える見通しが立ちました。

AI活用と開発過程での学び

開発中はAIに繰り返し質問し、プロセス間通信や文字コード処理の壁を一つずつ突破していきました。

実現が難しそうな要件でも、躊躇せずAIに質問を投げることで、新しい視点や解決の糸口が得られました。最終的には、AIの提案を基に自分で検証・修正を重ねることで、期待通りに動作するスクリプトに仕上げることができました。

AIとの協働は、開発のスピードと発想の広がりを確実に高めてくれると実感しています。

スクリプト

#HotIf WinActive("ahk_exe notepad++.exe")
/**
 * Notepad++の選択範囲を取得して返す。
 * 選択範囲がない場合、-1を返す。
 */
notepadPlusPlus_GETSELTEXT() {
	SCI_GETSELECTIONSTART := 2143
	SCI_GETSELECTIONEND := 2145
	SCI_GETSELTEXT := 2161
	PROCESS_ALL_ACCESS := 0x1F0FFF
	MEM_COMMIT_RESERVE := 0x1000 | 0x2000
	PAGE_READWRITE := 0x04
	MEM_RELEASE := 0x8000

	npHwnd := WinGetID("A")
	sciHwnd := ControlGetHwnd("Scintilla1", "ahk_id " npHwnd)

	pid := WinGetPID("ahk_id " npHwnd)
	hProcess := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", False, "UInt", pid, "Ptr")

	if !hProcess {
		MsgBox "プロセスオープンに失敗"
		ExitApp
	}

	; 選択範囲の長さ取得
	startPos := SendMessage(SCI_GETSELECTIONSTART, 0, 0, sciHwnd)
	endPos := SendMessage(SCI_GETSELECTIONEND, 0, 0, sciHwnd)
	selLen := endPos - startPos

	; 選択範囲がない場合、-1を返す。
	if selLen <= 0 {
		DllCall("CloseHandle", "Ptr", hProcess)
		return -1
	}

	; リモートプロセスにバッファ確保(文字数+ヌル文字)
	remoteBuf := DllCall("VirtualAllocEx", "Ptr", hProcess, "Ptr", 0, "UPtr", selLen + 1, "UInt", MEM_COMMIT_RESERVE, "UInt", PAGE_READWRITE, "Ptr")

	if !remoteBuf {
		MsgBox "VirtualAllocExに失敗"
		DllCall("CloseHandle", "Ptr", hProcess)
		ExitApp
	}

	; 第2引数にNULLを渡すことで必要なバイト数を取得
	n_seltext_bytes := SendMessage(SCI_GETSELTEXT, 0, 0, sciHwnd)
	if n_seltext_bytes != selLen {
		MsgBox "Assertion Error, n_seltext_bytes != selLen"
		ExitApp
	}

	SendMessage(SCI_GETSELTEXT, 0, remoteBuf, sciHwnd)

	localBuf := Buffer(selLen + 1)

	; リモートプロセスからテキスト読み込み
	DllCall("ReadProcessMemory", "Ptr", hProcess, "Ptr", remoteBuf, "Ptr", localBuf.Ptr, "UPtr", selLen + 1, "UPtr*", 0)

	selText := StrGet(localBuf.Ptr, selLen, "UTF-8")

	; リソース解放
	DllCall("VirtualFreeEx", "Ptr", hProcess, "Ptr", remoteBuf, "UPtr", 0, "UInt", MEM_RELEASE)
	DllCall("CloseHandle", "Ptr", hProcess)

	return selText
}

/**
 * Notepad++の選択範囲をreplace_textと置換する。
 * 置換後、replace_textを選択範囲にする。
 */
notepadPlusPlus_REPLACESEL(replace_text) {
	SCI_GETSELECTIONSTART := 2143
	SCI_SETSEL := 2160
	SCI_REPLACESEL := 2170
	PROCESS_ALL_ACCESS := 0x1F0FFF

	npHwnd := WinGetID("A")
	npPid := WinGetPID("A")
	sciHwnd := ControlGetHwnd("Scintilla1", "ahk_id " npHwnd)

	sel_start1 := SendMessage(SCI_GETSELECTIONSTART, 0, 0, , "ahk_id " sciHwnd)

	hProc := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", 0, "UInt", npPid, "Ptr")

	if !hProc {
		MsgBox("OpenProcess failed")
		ExitApp
	}

	txt_len := StrLen(replace_text)

	len := StrPut(replace_text, "UTF-8") ; UTF-8符号化された文字に必要なバイト数を計算(文字列の長さと異なる)
	buf := Buffer(len)
	StrPut(replace_text, buf.Ptr, "UTF-8")

	mem_addr := DllCall("VirtualAllocEx", "Ptr", hProc, "Ptr", 0, "UInt", len, "UInt", 0x3000, "UInt", 0x4, "Ptr")

	if !mem_addr {
		MsgBox("VirtualAllocEx failed")
		DllCall("CloseHandle", "Ptr", hProc)
		ExitApp
	}

	; リモートプロセスにテキスト書き込み
	DllCall("WriteProcessMemory", "Ptr", hProc, "Ptr", mem_addr, "Ptr", buf.Ptr, "UInt", len, "UInt*", 0)

	SendMessage(SCI_REPLACESEL, 0, mem_addr, , "ahk_id " sciHwnd)

	; リソース解放
	DllCall("VirtualFreeEx", "Ptr", hProc, "Ptr", mem_addr, "UInt", 0, "UInt", 0x8000)
	DllCall("CloseHandle", "Ptr", hProc)

	sel_start2 := SendMessage(SCI_GETSELECTIONSTART, 0, 0, , "ahk_id " sciHwnd)

	; 位置sel_start1からsel_start2までを選択範囲にする。
	SendMessage(SCI_SETSEL, sel_start1, sel_start2, , "ahk_id " sciHwnd)
}

/**
 * Notepad++において選択範囲を hoge → 「hoge」 → 『hoge』 → hoge でループする。
 */
括弧化() {
	s_sel := notepadPlusPlus_GETSELTEXT()

	if Type(s_sel) == "Integer" {
		MsgBox "選択範囲がありません。"
		return
	}

	if RegExMatch(s_sel, "s)^「(.*)」$", &m) {
		s_sel := "『" m[1] "』"
	} else if RegExMatch(s_sel, "s)^『(.*)』$", &m) {
		s_sel := m[1]
	} else {
		s_sel := "「" s_sel "」"
	}

	notepadPlusPlus_REPLACESEL(s_sel)
}

^+![:: 括弧化()
#HotIf

おわりに

AutoHotkeyからNotepad++を直接制御することで、Scintillaの豊富な機能を有効活用できる可能性が広がります。

今回の取り組みはその一例であり、今後はこの手法を基盤としたより高度なNotepad++拡張や自動化ツールの開発も期待できそうです。

Discussion